using System.Collections.Generic;
using NHibernate.Criterion;

namespace infoMECHANICA.NHibernateORM.CriteriaBuilder
{
    /// <summary>
    /// Wrapps NHibernate DetachedCriteria objects into one mergeable constuction
    /// </summary>
    public class MergeSet<R, Dto> : ICriteriaSource
    {
        private DetachedCriteria savedCriteria;
        private string savedIdPropertyName;

        /// <summary>
        /// Criteria - saved and wrapped
        /// </summary>
        protected DetachedCriteria SavedCriteria
        {
            get { return savedCriteria; }
            set { savedCriteria = value; }
        }

        /// <summary>
        /// Saved identifier property name what is necessary to join criterias together
        /// </summary>
        protected string SavedIdPropertyName
        {
            get { return savedIdPropertyName; }
            set { savedIdPropertyName = value; }
        }

        
        #region ICriteriaSource Members

        /// <summary>
        /// <see>ICriteriaSource</see> interface summary
        /// </summary>
        public DetachedCriteria BuildCriteria()
        {
            return BuildCriteria("root");
        }

        /// <summary>
        /// <see>ICriteriaSource</see> interface summary
        /// </summary>
        public DetachedCriteria BuildCriteria(string alias)
        {
            return this.SavedCriteria;
        }

        /// <summary>
        /// <see>ICriteriaSource</see> interface summary
        /// </summary>
        public bool HasCriteria()
        {
            return (this.savedCriteria != null);
        }

        #endregion       

        /// <summary>
        /// Looks into the collection of merge sets, trying to find some NHibernate
        /// DetachedCriterias which can be exposed as a ICriterions
        /// </summary>
        /// <param name="mergeSets">The table of MergeSets to search</param>
        /// <param name="idPropertyName">The name of class Id property name</param>
        /// <returns>ICriterions collection</returns>
        private static IList<ICriterion> FindCriterions(string idPropertyName, MergeSet<R, Dto>[] mergeSets)
        {
            IList<ICriterion> result = new List<ICriterion>();
            if (mergeSets.Length > 0)
            {
                for (int i = 0; i < mergeSets.Length; i++)
                {
                    //string subAliasName = String.Format("crit{0}", i);
                    DetachedCriteria subCriteria = mergeSets[i].SavedCriteria;
                    subCriteria.SetProjection(Projections.Id());

                    result.Add(Subqueries.PropertyIn(idPropertyName, subCriteria));
                }
            }

            return result;
        }

        /// <summary>
        /// Merges the MergeSets as a conjunction
        /// </summary>
        /// <param name="mergeSets">MergeSets to be merged</param>
        /// <param name="idPropertyName">The name of class Id property name</param>
        /// <returns>Merged MergeSet</returns>
        private static MergeSet<R, Dto> MergeAsConjuntion(string idPropertyName, params MergeSet<R, Dto>[] mergeSets)
        {
            DetachedCriteria criteria = DetachedCriteria.For(typeof(Dto));
            Conjunction and = Restrictions.Conjunction();

            IList<ICriterion> criterionsFound = FindCriterions(idPropertyName, mergeSets);
            foreach (ICriterion criterion in criterionsFound)
            {
                and.Add(criterion);
            }

            criteria.Add(and);
            return new MergeSet<R, Dto>(criteria, idPropertyName);
        }

        /// <summary>
        /// Merges the MergeSets as a disjunction
        /// </summary>
        /// <param name="mergeSets">MergeSets to be merged</param>
        /// <param name="idPropertyName">The name of class Id property name</param>
        /// <returns>Merged MergeSet</returns>
        private static MergeSet<R, Dto> MergeAsDisjuntion(string idPropertyName, params MergeSet<R, Dto>[] mergeSets)
        {
            DetachedCriteria criteria = DetachedCriteria.For(typeof(Dto));
            Disjunction or = Restrictions.Disjunction();

            IList<ICriterion> criterionsFound = FindCriterions(idPropertyName, mergeSets);
            foreach (ICriterion criterion in criterionsFound)
            {
                or.Add(criterion);
            }

            criteria.Add(or);
            return new MergeSet<R, Dto>(criteria, idPropertyName);
        }

        /// <summary>
        /// Merges two MergeSets together (as a conjuntion)
        /// </summary>
        /// <param name="set1">MergeSet to be merged (left side)</param>
        /// <param name="set2">MergeSet to te merges (right side)</param>
        /// <returns>Merged MergeSet object</returns>
        public static MergeSet<R, Dto> operator &(MergeSet<R, Dto> set1,
                                  MergeSet<R,Dto> set2)
        {
            MergeSet<R, Dto> result = MergeAsConjuntion(set1.SavedIdPropertyName, set1, set2);

            return result;
        }

        /// <summary>
        /// Merges two MergeSets together (as a Disjuntion)
        /// </summary>
        /// <param name="set1">MergeSet to be merged (left side)</param>
        /// <param name="set2">MergeSet to te merges (right side)</param>
        /// <returns>Merged MergeSet object</returns>
        public static MergeSet<R, Dto> operator |(MergeSet<R, Dto> set1,
                          MergeSet<R, Dto> set2)
        {
            MergeSet<R, Dto> result = MergeAsDisjuntion(set1.SavedIdPropertyName, set1, set2);

            return result;
        }

        /// <summary>
        /// Constructs MergeSet from existing DetachedCriteria object
        /// </summary>
        /// <param name="wrappedCriteria">DetachedCriteria which will be saved in MergeSet being created</param>
        /// <param name="idPropertyName">The name of class Id property name</param>
        public MergeSet(DetachedCriteria wrappedCriteria, string idPropertyName)
        {
            this.SavedCriteria = wrappedCriteria;
            this.SavedIdPropertyName = idPropertyName;
        }
    }
}
